Дата последнего изменения: 09.10.2024
Для операций записи используются три метода уже описанного нами класса: BookTable::add, BookTable::update, BookTable::delete.
Метод для добавления записи принимает на вход массив со значениями, где ключи - имена полей сущности:
namespace SomePartner\MyBooksCatalog; use Bitrix\Main\Type; $result = BookTable::add(array( 'ISBN' => '978-0321127426', 'TITLE' => 'Patterns of Enterprise Application Architecture', 'PUBLISH_DATE' => new Type\Date('2002-11-16', 'Y-m-d') )); if ($result->isSuccess()) { $id = $result->getId(); }
Метод возвращает объект результата Entity\AddResult, и в примере выше показано, как проверить успешность добавления и получить ID добавленной записи.
Обновление записи происходит похожим образом, только к массиву значений в параметрах добавляется значение первичного ключа:
$result = BookTable::update($id, array( 'PUBLISH_DATE' => new Type\Date('2002-11-15', 'Y-m-d') ));
В примере исправлена неправильно указанная при добавлении дата. В качестве результата возвращается объект Entity\UpdateResult, у которого так же есть проверочный метод isSuccess() (не было ли ошибок в запросе), и, дополнительно, можно узнать была ли запись фактически обновлена: getAffectedRowsCount().
Для удаления записи нужен только первичный ключ:
$result = BookTable::delete($id);
BookTable::delete(['key1' => value1, 'key2' => value2]);
Если во время операции произошла одна или несколько ошибок, их текст можно получить из результата:
$result = BookTable::update(...); if (!$result->isSuccess()) { $errors = $result->getErrorMessages(); }
Бывает, что у большинства новых записей значение какого-то поля всегда одно и то же, или вычисляется автоматически. Пусть у каталога книг дата издания/публикации по умолчанию будет сегодняшним днем (логично добавлять книгу в каталог сразу в день ее выхода). Вернемся к описанию поля в сущности и используем параметр `default_value`
:
new Entity\DateField('PUBLISH_DATE', array( 'default_value' => new Type\Date ))
Теперь при добавлении записи без явного указания даты издания ее значением будет текущий день:
$result = BookTable::add(array( 'ISBN' => '978-0321127426', 'TITLE' => 'Some new book' ));
Усложнение задачи: если нет возможности оперативно добавлять книги в день их выхода, но известно, что, как правило, новые книги выходят по пятницам. Соответственно, они добавлены будут только на следующей неделе:
new Entity\DateField('PUBLISH_DATE', array( 'default_value' => function () { // figure out last friday date $lastFriday = date('Y-m-d', strtotime('last friday')); return new Type\Date($lastFriday, 'Y-m-d'); } ))
Значением параметра `default_value`
может быть любой `callable`
: имя функции, массив из класса/объекта и названия метода, или анонимная функция.
Перед записью новых данных в БД нужно обязательно проверять их на корректность. Для этого предусмотрены валидаторы:
new Entity\StringField('ISBN', array( 'required' => true, 'column_name' => 'ISBNCODE', 'validation' => function() { return array( new Entity\Validator\RegExp('/[\d-]{13,}/') ); } ))
Теперь при добавлении и изменении записей ISBN будет проверен по шаблону [\d-]{13,}
- код должен содержать только цифры и дефис, минимум 13 цифр.
Валидация задается параметром 'validation'
в конструкторе поля и представляет собой callback, который возвращает массив валидаторов.
В качестве валидатора принимается наследник Entity\Validator\Base или любой callable
, который должен вернуть true
, или текст ошибки, или объект Entity\FieldError (в случае, если вы хотите использовать собственный код ошибки).
Точно известно, что в ISBN коде должно быть 13 цифр, эти цифры могут разделять несколько дефисов:
978-0321127426Чтобы удостовериться, что цифр там именно 13, напишем свой собственный валидатор:
new Entity\StringField('ISBN', array( 'required' => true, 'column_name' => 'ISBNCODE', 'validation' => function() { return array( function ($value) { $clean = str_replace('-', '', $value); if (preg_match('/^\d{13}$/', $clean)) { return true; } else { return 'Код ISBN должен содержать 13 цифр.'; } } ); } ))
Первым параметром в валидатор передается значение данного поля, но опционально доступно больше информации:
new Entity\StringField('ISBN', array( 'required' => true, 'column_name' => 'ISBNCODE', 'validation' => function() { return array( function ($value, $primary, $row, $field) { // value - значение поля // primary - массив с первичным ключом, в данном случае [ID => 1] // row - весь массив данных, переданный в ::add или ::update // field - объект валидируемого поля - Entity\StringField('ISBN', ...) } ); } ))
С таким набором данных можно произвести гораздо больший спектр сложных проверок.
Если к полю приписано несколько валидаторов, и есть необходимость программно узнать, какой конкретно из них сработал, можно воспользоваться кодом ошибки. Например, у кода ISBN последняя цифра - контрольная, служит для проверки правильности числовой части ISBN. Надо добавить валидатор для ее проверки и обработаем его результат особым образом:
// описываем валидатор в поле сущности new Entity\StringField('ISBN', array( 'required' => true, 'column_name' => 'ISBNCODE', 'validation' => function() { return array( function ($value) { $clean = str_replace('-', '', $value); if (preg_match('/^\d{13}$/', $clean)) { return true; } else { return 'Код ISBN должен содержать 13 цифр.'; } }, function ($value, $primary, $row, $field) { // проверяем последнюю цифру // ... // если цифра неправильная - возвращаем особую ошибку return new Entity\FieldError( $field, 'Контрольная цифра ISBN не сошлась', 'MY_ISBN_CHECKSUM' ); } ); } ))
// выполняем операцию $result = BookTable::update(...); if (!$result->isSuccess()) { // смотрим, какие ошибки были выявлены $errors = $result->getErrors(); foreach ($errors as $error) { if ($error->getCode() == 'MY_ISBN_CHECKSUM') { // сработал наш валидатор } } }
По умолчанию есть 2 стандартных кода ошибки: BX_INVALID_VALUE
, если сработал валидатор, и BX_EMPTY_REQUIRED
, если при добавлении записи не указано обязательное required поле.
Валидаторы срабатывают как при добавлении новых записей, так и при обновлении существующих. Такое поведение исходит из общего назначения валидаторов - гарантировать корректные и целостные данные в БД. Для проверки данных только при добавлении или только при обновлении, а также для других манипуляций существует механизм событий.
В типовых случаях рекомендуем вам использовать штатные валидаторы:
Описанные валидаторы не применимы к Пользовательским полям — проверка их значений конфигурируется в настройках поля через административный интерфейс.
В примере с валидаторами одной из проверок поля ISBN была проверка на наличие 13 цифр. Помимо цифр, в ISBN коде могут встречаться дефисы, но с технической точки зрения они не несут никакой ценности. Чтобы хранить в БД "чистые" данные - только 13 цифр, без дефисов - можно воспользоваться внутренним обработчиком события:
class BookTable extends Entity\DataManager { ... public static function onBeforeAdd(Entity\Event $event) { $result = new Entity\EventResult; $data = $event->getParameter("fields"); if (isset($data['ISBN'])) { $cleanIsbn = str_replace('-', '', $data['ISBN']); $result->modifyFields(array('ISBN' => $cleanIsbn)); } return $result; } }
Метод onBeforeAdd, определенный в сущности, автоматически распознается системой как обработчик события "перед добавлением", и в нем можно изменить данные или провести дополнительные проверки. В приведенном примере мы изменили поле ISBN посредством метода `modifyFields`.
// до преобразования 978-0321127426 978-1-449-31428-6 9780201485677 // после преобразования 9780321127426 9781449314286 9780201485677
После такого преобразования можно вновь вернуться к лаконичному валидатору RegExp вместо анонимной функции (ведь мы уже знаем, что допустимых дефисов в значении не будет, должны остаться только цифры):
'validation' => function() { return array( //function ($value) { // $clean = str_replace('-', '', $value); // // if (preg_match('/^\d{13}$/', $clean)) // { // return true; // } // else // { // return 'Код ISBN должен содержать 13 цифр.'; // } //}, new Entity\Validator\RegExp('/\d{13}/'), ... ); }
Помимо изменения данных, в обработчике события можно удалить данные или вовсе прервать выполнение операции. Например, необходимо запретить обновление ISBN кода для уже существующих в каталоге книг. Сделать это можно в событии onBeforeUpdate двумя способами:
public static function onBeforeUpdate(Entity\Event $event) { $result = new Entity\EventResult; $data = $event->getParameter("fields"); if (isset($data['ISBN'])) { $result->unsetFields(array('ISBN')); } return $result; }
В таком варианте ISBN будет "тихо" удален из набора данных, будто его и не передавали. Второй способ запретить его обновлять - сгенерировать ошибку:
public static function onBeforeUpdate(Entity\Event $event) { $result = new Entity\EventResult; $data = $event->getParameter("fields"); if (isset($data['ISBN'])) { $result->addError(new Entity\FieldError( $event->getEntity()->getField('ISBN'), 'Запрещено менять ISBN код у существующих книг' )); } return $result; }
В случае возврата ошибки мы сформировали объект Entity\FieldError для того, чтобы впоследствии при обработке ошибок знать, на каком именно поле сработала проверка. Если ошибка относится к нескольким полям или целиком ко всей записи, то более уместно будет воспользоваться объектом Entity\EntityError:
public static function onBeforeUpdate(Entity\Event $event) { $result = new Entity\EventResult; $data = $event->getParameter("fields"); if (...) // комплексная проверка данных { $result->addError(new Entity\EntityError( 'Невозможно обновить запись' )); } return $result; }
В примерах использовались два события: onBeforeAdd и onBeforeUpdate, всего же таких событий девять:
Порядок вызова событий и допустимые действия в обработчиках каждого из них:
Конечно же, обрабатывать эти события можно не только в самой сущности в одноименных методах. Чтобы подписаться на событие в произвольном месте выполнения скрипта, нужно вызвать менеджер событий:
$em = \Bitrix\Main\ORM\EventManager::getInstance(); $em->addEventHandler( BookTable::class, // класс сущности DataManager::EVENT_ON_BEFORE_ADD, // код события function () { // ваш callback var_dump('handle entity event'); } );
Иногда может возникнуть необходимость хранить данные в одном формате, а работать с ними в программе уже в другом. Самый распространенный пример: работа с массивом и его сериализация перед сохранением в БД. На этот случай предусмотрены параметры поля 'save_data_modification'
и 'fetch_data_modification'
. Определяются они аналогично валидаторам, через callback.
На примере каталога книг опишем текстовое поле EDITIONS_ISBN: оно будет хранить коды ISBN других изданий книги, если таковые имеются:
new Entity\TextField('EDITIONS_ISBN', array( 'save_data_modification' => function () { return array( function ($value) { return serialize($value); } ); }, 'fetch_data_modification' => function () { return array( function ($value) { return unserialize($value); } ); } ))
В параметре save_data_modification
мы указали сериализацию значения перед сохранением в БД, а в параметре fetch_data_modification
рассериализацию при выборке из БД. Теперь при написании бизнес-логики вы можете просто работать с массивом, не отвлекаясь на вопросы конвертации.
Поскольку сериализация - это наиболее типичный пример для конвертации значений, она вынесена в отдельный параметр serialized
:
new Entity\TextField('EDITIONS_ISBN', array( 'serialized' => true ))
Но вы по-прежнему можете описать свои callable для других вариантов модификации данных.
Очень часто разработчики сталкиваются с реализацией счетчиков, где для целостности данных предпочтительно рассчитывать новое значение на стороне БД, вместо выборки старого значения и пересчете его на стороне приложения. Другими словами, нужно выполнять запросы вида:
UPDATE my_book SET READERS_COUNT = READERS_COUNT + 1 WHERE ID = 1
Если описать числовое поле READERS_COUNT в сущности, то инкремент счетчика можно будет запустить следующим образом:
BookTable::update($id, array( 'READERS_COUNT' => new DB\SqlExpression('?# + 1', 'READERS_COUNT') ));
Плейсхолдер ?#
означает, что следующим аргументом в конструкторе идет идентификатор БД - имя базы данных, таблицы или колонки, и это значение будет экранировано соответствующим образом. Для всех изменяемых параметров рекомендуется обязательно использовать плейсхолдеры - такой подход поможет избежать проблем с SQL инъекциями.
Например, если инкрементируемое число читателей переменно, то лучше описать выражение так:
// правильно BookTable::update($id, array( 'READERS_COUNT' => new DB\SqlExpression('?# + ?i', 'READERS_COUNT', $readersCount) )); // неправильно BookTable::update($id, array( 'READERS_COUNT' => new DB\SqlExpression('?# + '.$readersCount, 'READERS_COUNT') ));
Список доступных на данный момент плейсхолдеров:
?
или ?s
- значение экранируется и заключается в кавычки '?#
- значение экранируется как идентификатор?i
- значение приводится к integer?f
- значение приводится к floatВ предыдущих примерах есть нюанс: запрос на обновление данных вызывается без проверки результата:
// вызов без проверки успешности выполнения запроса BookTable::update(...); // с проверкой $result = BookTable::update(...); if (!$result->isSuccess()) { // обработка ошибки }
Несомненно, второй вариант более предпочтителен с точки зрения контроля происходящего. Но если код выполняется только в режиме агента, нам некому и незачем показывать список возникших в процессе валидации ошибок. В таком случае, если запрос не прошел из-за "проваленной" валидации, и не была вызвана проверка isSuccess(), система сгенерирует E_USER_WARNING
со списком ошибок, который можно будет увидеть в логе сайта (если соответствующим образом настроить .settings.php).
По результатам данной главы произошли некоторые изменения в описании сущности, теперь оно выглядит так:
namespace SomePartner\MyBooksCatalog; use Bitrix\Main\Entity; use Bitrix\Main\Type; class BookTable extends Entity\DataManager { public static function getTableName() { return 'my_book'; } public static function getUfId() { return 'MY_BOOK'; } public static function getMap() { return array( new Entity\IntegerField('ID', array( 'primary' => true, 'autocomplete' => true )), new Entity\StringField('ISBN', array( 'required' => true, 'column_name' => 'ISBNCODE', 'validation' => function() { return array( new Entity\Validator\RegExp('/\d{13}/'), function ($value, $primary, $row, $field) { // проверяем последнюю цифру // ... // если цифра неправильная - возвращаем особую ошибку return new Entity\FieldError( $field, 'Контрольная цифра ISBN не сошлась', 'MY_ISBN_CHECKSUM' ); } ); } )), new Entity\StringField('TITLE'), new Entity\DateField('PUBLISH_DATE', array( 'default_value' => function () { // figure out last friday date $lastFriday = date('Y-m-d', strtotime('last friday')); return new Type\Date($lastFriday, 'Y-m-d'); } )), new Entity\TextField('EDITIONS_ISBN', array( 'serialized' => true )), new Entity\IntegerField('READERS_COUNT') ); } public static function onBeforeAdd(Entity\Event $event) { $result = new Entity\EventResult; $data = $event->getParameter("fields"); if (isset($data['ISBN'])) { $cleanIsbn = str_replace('-', '', $data['ISBN']); $result->modifyFields(array('ISBN' => $cleanIsbn)); } return $result; } }
Скопировав этот код, вы можете поэкспериментировать со всеми описанными выше возможностями.